package org.openmrs.module.simplelabentry.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Concept;
import org.openmrs.ConceptAnswer;
import org.openmrs.ConceptSet;
import org.openmrs.Encounter;
import org.openmrs.Location;
import org.openmrs.Order;
import org.openmrs.OrderType;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.Role;
import org.openmrs.User;
import org.openmrs.api.APIException;
import org.openmrs.api.OrderService.ORDER_STATUS;
import org.openmrs.api.context.Context;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.hospitalcore.LabService;
import org.openmrs.module.hospitalcore.model.Lab;
import org.openmrs.module.hospitalcore.model.LabTest;
import org.openmrs.module.hospitalcore.util.HospitalCoreConstants;
import org.openmrs.module.simplelabentry.SimpleLabEntryService;
import org.openmrs.module.simplelabentry.db.SimpleLabEntryDAO;
import org.openmrs.module.simplelabentry.report.ConceptColumn;
import org.openmrs.module.simplelabentry.report.DataSet;
import org.openmrs.module.simplelabentry.report.ExcelReportRenderer;
import org.openmrs.module.simplelabentry.report.LabOrderReport;
import org.openmrs.module.simplelabentry.util.DateUtil;
import org.openmrs.module.simplelabentry.util.SimpleLabEntryUtil;
import org.openmrs.report.DataSetDefinition;
import org.openmrs.report.EvaluationContext;
import org.openmrs.util.OpenmrsUtil;

/**
 * Default implementation of the SimpleLabEntry-related services class.
 * 
 * This method should not be invoked by itself. Spring injection is used to
 * inject this implementation into the ServiceContext. Which implementation is
 * injected is determined by the spring application context file:
 * /metadata/api/spring/applicationContext.xml
 * 
 * @see org.openmrs.module.simplelabentry.SimpleLabEntryService
 */

class WorkListComparator implements Comparator<Map<String, String>>{

	@Override
	public int compare(Map<String, String> o1, Map<String, String> o2) {
		String name1 = o1.get("Name");
		String name2 = o2.get("Name");
		return name1.compareToIgnoreCase(name2);		
	}	
}

public class SimpleLabEntryServiceImpl extends BaseOpenmrsService implements
		SimpleLabEntryService {

	protected static final Log log = LogFactory
			.getLog(SimpleLabEntryServiceImpl.class);
	protected SimpleLabEntryDAO dao;

	public void setDao(SimpleLabEntryDAO dao) {
		this.dao = dao;
	}

	public List<Order> getOrders(List<Concept> concepts, OrderType orderType,
			List<Patient> patientList, Location location, Date orderDate) {
		return dao.getOrders(concepts, orderType, patientList, location,
				orderDate);
	}

	/**
	 * @see org.openmrs.module.simplelabentry.SimpleLabEntryService#getOrders(
	 *      OrderType, Concept, Location, Date, ORDER_STATUS, List<Patient>)
	 */
	public List<Order> getLabOrders(Concept concept, Location location,
			Date orderDate, ORDER_STATUS status, List<Patient> patients,
			Set<Concept> listSupportedLabSets, OrderType orderType) {

		List<Order> orderList = new ArrayList<Order>();
		// log.info("retriving order for concpet "+concept+" location = "+location
		// + " od = "+orderDate + " status = "+status+ " patients = "+patients);
		// Retrieve proper OrderType for Lab Orders

		List<Concept> conceptList = new ArrayList<Concept>();
		if (concept == null) {
			for (Concept labSetConcept : listSupportedLabSets) {
				for (Concept conceptSetMemeber : getAnswers(labSetConcept)) {
					try {
						conceptList.add(conceptSetMemeber);
					} catch (Exception e) {
					}
				}
			}
		} else {
			conceptList = Arrays.asList(concept);
		}

		if (status == null) {
			status = ORDER_STATUS.NOTVOIDED;
		}

		Date now = new Date();

		// Retrieve matching orders
		// we need to speed this up:
		// log.info("Retrieving lab orders for: concept=" + conceptList +
		// ",date=" + orderDate + ",status=" + status );
		List<Order> ordersMatch = getOrders(conceptList, orderType, patients,
				location, orderDate);
		// log.info("lab orders size " + ordersMatch.size() );
		for (Order o : ordersMatch) {
			// TODO: This shouldn't be necessary, but it seems like the
			// OrderService does not do it correctly?
			if (status != null) {
				if ((status == ORDER_STATUS.CURRENT && o.isDiscontinued(now))
						|| (status == ORDER_STATUS.COMPLETE && o.isCurrent())) {
					continue;
				}
			}
			orderList.add(o);
		}
		// log.info("returning " + orderList.size() + " lab orders");
		return orderList;
	}

	/**
	 * @see org.openmrs.module.simplelabentry.SimpleLabEntryService#getLabTestConceptSets()
	 */

	public List<Concept> getAnswers(Concept labSet) {
		List<Concept> conceptList = new ArrayList<Concept>();
		if (labSet.getDatatype().isCoded()) {
			if (!labSet.getAnswers().isEmpty()) {
				List<ConceptAnswer> conceptAnswers = new ArrayList<ConceptAnswer>(
						labSet.getAnswers());
				for (int count = 0; count < conceptAnswers.size(); count++) {
					Concept conceptAnsName = conceptAnswers.get(count)
							.getAnswerConcept();
					conceptList.add(conceptAnsName);
				}
			}
		}
		return conceptList;
	}

	/**
	 * Get a list of concept IDs
	 * 
	 * @return
	 */
	public List<Concept> getSupportedLabConcepts() {
		// orders the appropriate concepts according to requirements
		return SimpleLabEntryUtil.getLabReportConcepts();
	}

	/**
	 * Get a list of concept columns.
	 * 
	 * TODO If we need to override values like precision, we'll do it here.
	 * 
	 * @return
	 */
	public List<ConceptColumn> getConceptColumns(String labName) {
		List<ConceptColumn> columns = new ArrayList<ConceptColumn>();
		for (Concept concept : getAnswers(Context.getConceptService()
				.getConcept(labName))) {
			try {
				// System.out.println("concept "+concept.getName());
				if (concept.getConceptClass().getName()
						.equalsIgnoreCase("LabSet")) {
					Collection<ConceptSet> cSets = concept.getConceptSets();
					for (ConceptSet concept1 : cSets) {
						Concept c1 = concept1.getConcept();
						// System.out.println("c1 "+c1.getName());
						if (c1.getConceptClass().getName()
								.equalsIgnoreCase("Test")) {
							ConceptColumn col = new ConceptColumn(c1);
							// System.out.println("c1 "+c1.getName());
							if (!columns.contains(col)) {
								columns.add(col);
								// System.out.println("adding in column "+col.getDisplayName());
							}
						}
					}
				} else {

					if (concept.getConceptClass().getName()
							.equalsIgnoreCase("Test")) {
						ConceptColumn col = new ConceptColumn(concept);
						// System.out.println("concept "+concept.getName());
						if (!columns.contains(col)) {
							columns.add(col);
							// System.out.println("adding in column "+col.getDisplayName());
						}
					}
				}
			} catch (Exception e) {
				log.error(
						"Error occurred while looking up concept / concept set by ID "
								+ concept.getConceptId(), e);
				throw new APIException("Invalid concept ID "
						+ concept.getConceptId() + " : " + e.getMessage(), e);
			}
		}
		System.out.println("columns size = " + columns.size());
		return columns;
	}

	/**
     * 
     */
	public LabOrderReport runLabOrderReport(Location location, Date startDate,
			Date endDate, String labName, Set<Concept> listSupportedLabSets,
			PatientIdentifierType identifierType, OrderType orderType) {

		LabOrderReport report = new LabOrderReport();
		// report.addParameter("location", location);
		report.addParameter("startDate", startDate);
		report.addParameter("endDate", endDate);
		report.addParameter("labName", labName);

		// List<ConceptColumn> columns = getConceptColumns();
		// TODO We should pass in the report here, but at the time I just needed
		// to add parameters
		// so that we could have access to them when we render the report.
		List<Map<String, String>> dataSet = getLabOrderReportData(location,
				startDate, endDate, labName, listSupportedLabSets,
				identifierType, orderType);

		report.setDataSet(new DataSet(dataSet));

		return report;
	}

	/**
     * 
     */
	public File runAndRenderLabOrderReport(Location location, Date startDate,
			Date endDate, String labName, Set<Concept> listSupportedLabSets,
			PatientIdentifierType identifierType, OrderType orderType)
			throws IOException {

		FileOutputStream fos = null;
		try {

			LabOrderReport report = runLabOrderReport(location, startDate,
					endDate, labName, listSupportedLabSets, identifierType,
					orderType);
			ExcelReportRenderer renderer = new ExcelReportRenderer();
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ssZ");
//			File file = new File(
//					OpenmrsUtil
//							.getDirectoryInApplicationDataDirectory("SimpleLabEntry"),
//					"lab-order-report" + sdf.format(new Date()) + ".xls");
			File file = new File(
					OpenmrsUtil
							.getDirectoryInApplicationDataDirectory("SimpleLabEntry"),
					labName + "-" + sdf.format(new Date()) + ".xls");
			fos = new FileOutputStream(file);
			renderer.render(report, fos);
			return file;

		} catch (Exception e) {
			log.error("Unable to render lab order report", e);
		} finally {
			if (fos != null) {
				fos.flush();
				fos.close();
			}
		}
		return null;
	}

	/**
	 * @see DataSetEvaluator#evaluate(DataSetDefinition, EvaluationContext)
	 */
	public List<Map<String, String>> getLabOrderReportData(Location location,
			Date startDate, Date endDate, String labName,
			Set<Concept> listSupportedLabSets,
			PatientIdentifierType identifierType, OrderType orderType) {

		List<Map<String, String>> dataset = new ArrayList<Map<String, String>>();

		Concept labConcept = Context.getConceptService().getConcept(labName);
		Set<Concept> allTestsList = new HashSet<Concept>();
		for (Concept conceptSetMemeber : getAnswers(labConcept)) {
			allTestsList.add(conceptSetMemeber);
		}

		// get list order from labtest
		SimpleLabEntryService ls = Context
				.getService(SimpleLabEntryService.class);
		LabService labService = Context.getService(LabService.class);
		List<Order> workList = new ArrayList<Order>();
		Lab currentLab = ls.getCurrentLab();
		// System.out.println("currentLab: "+currentLab);
		List<LabTest> listLabTest = labService.getLatestLabTestByDate(
				currentLab, startDate);
		Order ord = null;
		Set<Encounter> encounters = new HashSet<Encounter>();
		Map<Encounter,Set<Order>> mapOrders = new HashMap<Encounter, Set<Order>>();
		Encounter enc = null;
		Set<Order> orders = null;
		for (LabTest test : listLabTest) {
			ord = test.getOrder();
			// System.out.println("disc: "+order.getDiscontinued());
			if (!ord.getDiscontinued()) {
				workList.add(ord);
				enc = ord.getEncounter();
				encounters.add(enc);
				 orders = mapOrders.get(enc);
				 if( orders == null){
					 orders = new HashSet<Order>();
				 }
				 orders.add(ord);
				 mapOrders.remove(enc);
				 mapOrders.put(enc, orders);
			}
		}

//		Set<Encounter> encounters = getLabOrderEncounters(location, startDate,
//				endDate, labName, allTestsList, orderType);
		log.info("Encounters found: " + encounters.size());

		Map<Encounter, List<Order>> mapEncounterOrder = new HashMap<Encounter, List<Order>>();
		Map<Order, List<Concept>> mapOrderConcept = new HashMap<Order, List<Concept>>();
		Map<Integer, String> mapLabsetName = new HashMap<Integer, String>();
		for (Encounter encounter : encounters) { // used to be
													// mergedEncounters.value
			// log.info("Encounter for " + encounter.getPatient() + " on " +
			// encounter.getEncounterDatetime());

			Set<Order> oSet = mapOrders.get(encounter);
			
			List<Order> listOrder = new ArrayList<Order>();

			Concept orderConcept = null;
			for (Order o : oSet) {
				orderConcept = o.getConcept();
				if (allTestsList.contains(orderConcept)) {

					// row.put("Test Ordered",
					// o.getConcept().getName().getShortName().isEmpty() ?
					// o.getConcept().getName().getName() :
					// o.getConcept().getName().getShortName());
					listOrder.add(o);

					List<Concept> orderedTestNamesList = new ArrayList<Concept>();
					if (orderConcept.getConceptClass().getName()
							.equalsIgnoreCase("LabSet")) {
						Collection<ConceptSet> cSets = orderConcept
								.getConceptSets();
						for (ConceptSet concept1 : cSets) {
							Concept c1 = concept1.getConcept();
							if (c1.getConceptClass().getName()
									.equalsIgnoreCase("Test")) {
								if (!orderedTestNamesList.contains(c1)) {
									mapLabsetName.put(c1.getConceptId(),
											orderConcept.getName().getName());
									orderedTestNamesList.add(c1);
								}
							} else if (c1.getConceptClass().getName()
									.equalsIgnoreCase("LabSet")) {
								Collection<ConceptSet> cSets1 = c1
										.getConceptSets();
								for (ConceptSet concept2 : cSets1) {
									Concept c2 = concept2.getConcept();
									if (c2.getConceptClass().getName()
											.equalsIgnoreCase("Test")) {
										if (!orderedTestNamesList.contains(c2)) {
											mapLabsetName.put(
													c2.getConceptId(), c1
															.getName()
															.getName());
											orderedTestNamesList.add(c2);
										}
									}
								}
							}
						}
					} else {

						if (orderConcept.getConceptClass().getName()
								.equalsIgnoreCase("Test")) {
							if (!orderedTestNamesList.contains(orderConcept)) {
								orderedTestNamesList.add(orderConcept);
							}
						}
					}
					mapOrderConcept.put(o, orderedTestNamesList);
				}
			}
			mapEncounterOrder.put(encounter, listOrder);

		}

		System.out.println("identifier type : " + identifierType.getName());
		LabTest labTest = null;
		String labSet = null;
		for (Encounter encounter : encounters) {
			for (Order order : mapEncounterOrder.get(encounter)) {
				for (Concept concept : mapOrderConcept.get(order)) {
					Map<String, String> row = new LinkedHashMap<String, String>();
					PatientIdentifier pi = encounter.getPatient()
							.getPatientIdentifier(identifierType);
					row.put("Order date", DateUtil.formatDate(order.getDateCreated(), "dd/MM/yyyy"));
					if (pi != null) {
						row.put("Patient Identifier", pi.getIdentifier());
					} else {
						System.out.println("pi = null "
								+ encounter.getPatient().getNames());
						row.put("Patient Identifier", "");
					}					
					row.put("Lab", labName);
					String name = getFullName(encounter.getPatient());
					row.put("Name", name);
					row.put("Age ", DateUtil.getTimespan(new Date(), encounter
							.getPatient().getBirthdate()));
					row.put("Gender", encounter.getPatient().getGender());
					labSet = mapLabsetName.get(concept.getConceptId());
					row.put("Test", labSet != null ? labSet : "");
					row.put("Test Name", concept.getName().getShortName()
							.isEmpty() ? concept.getName().getName() : concept
							.getName().getShortName());
					labTest = labService.getLabTestByOrder(order);
					row.put("Sample No.",
							labTest != null ? labTest.getSampleNumber() : "");
					row.put("Enter Result",
							".                                                       .");
					labTest.setLabTestStatus(HospitalCoreConstants.LABTEST_STATUS_PRINTED);
					labService.saveLabTest(labTest);
					// Add row to dataset
					dataset.add(row);
				}				
				
				Context.getOrderService().saveOrder(order);
			}
		}
		
		// sort the dataset
		Collections.sort(dataset, new WorkListComparator());
		return dataset;
	}

	// Get the fullname of a patient
	private String getFullName(Patient patient) {
		String fullName = "";

		if (!StringUtils.isBlank(patient.getGivenName())) {
			fullName += patient.getGivenName() + " ";
		}

		if (!StringUtils.isBlank(patient.getMiddleName())) {
			fullName += patient.getMiddleName() + " ";
		}

		if (!StringUtils.isBlank(patient.getFamilyName())) {
			fullName += patient.getFamilyName();
		}

		fullName = StringUtils.trim(fullName);
		return fullName;
	}

	/**
	 * Get all lab related encounters during a period defined by the given start
	 * date and end date. The location parameter can be chosen to refine the
	 * results or ignored.
	 * 
	 * @param location
	 *            the location of the lab encounter
	 * @param startDate
	 *            the start date of period to be searched
	 * @param endDate
	 *            the end date of period to be searched
	 */
	public Set<Encounter> getLabOrderEncounters(Location location,
			Date startDate, Date endDate, String labName,
			Set<Concept> listSupportedLabSets, OrderType orderType) {

		Set<Encounter> encounterList = new HashSet<Encounter>();
		for (Order order : getLabOrdersBetweenDates(location, startDate,
				endDate, labName, listSupportedLabSets, orderType)) {

			Encounter encounter = order.getEncounter();
			encounterList.add(encounter);

			// encountersMap.put(encounter.getDateCreated(), encounter);
		}

		return encounterList;
	}

	/**
	 * Gets all non-voided lab orders in the system.
	 * 
	 * @return
	 */
	public List<Order> getAllLabOrders(OrderType orderType) {

		// Only show lab orders
		List<OrderType> orderTypes = new ArrayList<OrderType>();
		orderTypes.add(orderType);

		return Context.getOrderService().getOrders(Order.class, null, null,
				ORDER_STATUS.NOTVOIDED, null, null, orderTypes);
	}

	/**
	 * Gets all lab orders that were completed with the given period at the
	 * given location.
	 * 
	 * @param location
	 *            the location of the lab encounter
	 * @param startDate
	 *            the start date of period to be searched
	 * @param endDate
	 *            the end date of period to be searched
	 */
	public List<Order> getLabOrdersBetweenDates(Location location,
			Date startDate, Date endDate, String labName,
			Set<Concept> listSupportedLabSets, OrderType orderType) {

		// TODO need to implement DAO layer method to get orders by the given
		// parameters
		List<Order> orders = new ArrayList<Order>();

		// FIXME This should be done in the service OR dao layer
		Concept concept = Context.getConceptService().getConcept(labName);

		if (concept == null) {
			throw new RuntimeException("Unable to retrieve LabTest = "
					+ labName + ". Please check global property configuration.");
		}
		// System.out.println("concept  = " + concept.getName());
		for (Concept conceptSet : getAnswers(concept)) {
			// System.out.println("conceptSet  = " + conceptSet.getName());
			List<Order> setOrderList = getLabOrders(conceptSet, location,
					startDate, ORDER_STATUS.CURRENT, null,
					listSupportedLabSets, orderType);
			// System.out.println("setOrderList size = " + setOrderList.size());
			for (Order order : setOrderList) {
				if (order.getAccessionNumber() != null) {
					if (order.getAccessionNumber().equals("1")) {
						// System.out.println("order.getAccessionNumber() = "+order.getAccessionNumber());
						orders.add(order);
					}
				}

			}
		}

		Collections.sort(orders, new Comparator<Order>() {

			public int compare(Order o1, Order o2) {
				return o1.getPatient().getPatientId()
						.compareTo(o2.getPatient().getPatientId());
			}
		});
		log.info("orders size = " + orders.size());
		return orders;

	}

	@Override
	public Set<Concept> getSupportedLabSets() throws APIException {
		User user = Context.getAuthenticatedUser();
		LabService labService = Context.getService(LabService.class);
		List<Role> roles = new ArrayList<Role>(user.getAllRoles());
		List<Lab> labs = labService.getLabByRoles(roles);
		if (labs != null && labs.size() > 0) {
			return labs.get(0).getInvestigationsToDisplay();
		} else
			return null;
	}

	@Override
	public Lab getCurrentLab() throws APIException {
		User user = Context.getAuthenticatedUser();
		if (user == null) {
			return null;
		}
		LabService labService = Context.getService(LabService.class);
		List<Role> roles = new ArrayList<Role>(user.getAllRoles());
		List<Lab> labs = labService.getLabByRoles(roles);
		if (labs != null && labs.size() > 0) {
			return labs.get(0);
		} else
			return null;
	}

}
